/*
 * Copyright Hundsun Technologies Inc. All Rights Reserved.
 */

package selector

import (
	"context"
	"errors"
	"fmt"
	"math/rand"
	"strconv"
	"time"

	"github.com/golang/protobuf/proto"
	"hundsun.com/hsl/hschain/common"
	"hundsun.com/hsl/hschain/common/config"
	"hundsun.com/hsl/hschain/common/keystore"
	"hundsun.com/hsl/hschain/common/log/logging"
	dposcomm "hundsun.com/hsl/hschain/consensus/impl/dpos/common"
	"hundsun.com/hsl/hschain/module/common/utils"
	"hundsun.com/hsl/hschain/protos"
	pbcomm "hundsun.com/hsl/hschain/protos/common"
	pbdpos "hundsun.com/hsl/hschain/protos/consensus/dpos"
	pbledger "hundsun.com/hsl/hschain/protos/ledger"
)

type voteConfig struct {
	//是否竞选Miner
	CompeteMiner bool

	//投票人地址
	VoterAddress protos.Address

	//生效轮次（含）
	StartRound int64

	//失效轮次（不含）
	EndRound int64

	//voter网络PeerID地址（CompeteMiner为true时必须）
	VoterPeerID string

	//代理人地址（CompeteMiner为false时必须）
	AgentAddress protos.Address
}

func (c *voteConfig) String() string {
	return fmt.Sprintf("voteConfig[competeMiner=%v,agentAddress=%s,minerPeerID=%s,startRound=%d,endRound=%d]", c.CompeteMiner, c.AgentAddress, c.VoterPeerID, c.StartRound, c.EndRound)
}

// Voter 根据选举模块配置进行投票
type Voter struct {
	module *DposSelector
	log    logging.Log
	ctx    context.Context
}

// Run 选举模块i配置文件判断是否需要投票，如果需要，则通过P2P广播交易
func (v *Voter) Run() {
	v.log.Info("start dpos voter...")
	defer v.log.Info("exit dpos voter...")
	electCtx := v.module.electionProgress.Load().Clone()
	currentRound, _, err := dposcomm.CalculateRound(time.Now().UnixNano() / int64(time.Millisecond))
	if err != nil {
		panic(err)
	}
	voteConf, voterAccount, err := parseVoteConfig(v.module.config, currentRound)
	if err != nil {
		v.log.Infof("skip voting because of voter config error:%s", err.Error())
		return
	}
	if voteConf == nil {
		v.log.Info("skip voting because fo voter config is empty or broken")
		return
	}
	candidateOfVoter := electCtx.Candidates[voterAccount.GetAddress()]
	delegationOfVoter := electCtx.Delegations[voterAccount.GetAddress()]
	ballots := createBallots(candidateOfVoter, delegationOfVoter, voteConf)
	if len(ballots) > 0 {
		ballotsMsg := &pbdpos.Ballots{
			Ballots: ballots,
		}
		ballotsPayload, err := proto.Marshal(ballotsMsg)
		if err != nil {
			v.log.Error(err)
			return
		}
		txPayload := &pbcomm.BlackHoleTransactionPayload{
			Type:    uint32(pbdpos.BlackHoleTransactionPayloadTypeBallots),
			Payload: ballotsPayload,
		}
		txPayloadBytes, err := proto.Marshal(txPayload)
		if err != nil {
			v.log.Error(err)
			return
		}

		tx := &pbledger.Transaction{
			From:       string(voterAccount.GetAddress()),
			To:         string(common.BlackHoleAddress),
			ContractId: nil,
			Value:      0,
			Fee:        0,
			Payload:    txPayloadBytes,
			Nonce:      rand.Uint64(),
			CreateTime: time.Now().UnixNano() / int64(time.Millisecond),
		}
		tx.Sign(voterAccount.GetKeyPair())

	}
}

func createBallots(candidate *Candidate, delegation *Delegation, voteConf *voteConfig) []*pbdpos.Ballot {
	var ballots []*pbdpos.Ballot
	if candidate != nil {
		if voteConf == nil {
			ballots = append(ballots, buildRevokeNominationBallot())
			return ballots
		}
		if !voteConf.CompeteMiner {
			ballots = append(ballots, buildRevokeNominationBallot())
			ballots = append(ballots, buildDelegationBallot(voteConf))
			return ballots
		}
		shouldCreateCandidateTx := false
		if candidate.StartRound != voteConf.StartRound {
			shouldCreateCandidateTx = true
		}
		if candidate.EndRound != voteConf.EndRound {
			shouldCreateCandidateTx = true
		}
		if candidate.PeerID != voteConf.VoterPeerID {
			shouldCreateCandidateTx = true
		}
		if shouldCreateCandidateTx {
			ballots = append(ballots, buildNominationBallot(voteConf))
		}
		return ballots
	}

	if delegation != nil {
		if voteConf == nil {
			ballots = append(ballots, buildRevokeDelegationBallot())
			return ballots
		}
		if voteConf.CompeteMiner {
			ballots = append(ballots, buildRevokeDelegationBallot())
			ballots = append(ballots, buildNominationBallot(voteConf))
			return ballots
		}
		shouldCreateDelegateTx := false
		if delegation.StartRound != voteConf.StartRound {
			shouldCreateDelegateTx = true
		}
		if delegation.EndRound != voteConf.EndRound {
			shouldCreateDelegateTx = true
		}
		if delegation.AgentAddress != voteConf.AgentAddress {
			shouldCreateDelegateTx = true
		}
		if shouldCreateDelegateTx {
			ballots = append(ballots, buildDelegationBallot(voteConf))
		}
		return ballots
	}

	if voteConf != nil {
		if voteConf.CompeteMiner {
			ballots = append(ballots, buildNominationBallot(voteConf))
		} else {
			ballots = append(ballots, buildDelegationBallot(voteConf))
		}
	}
	return ballots
}

func buildNominationBallot(voteConf *voteConfig) *pbdpos.Ballot {
	ballot := &pbdpos.Ballot{
		Type: pbdpos.Ballot_TypeNomination,
		Content: &pbdpos.Ballot_Nomination{Nomination: &pbdpos.NominationBallotContent{
			PeerID:     voteConf.VoterPeerID,
			StartRound: voteConf.StartRound,
			EndRound:   voteConf.EndRound,
		}},
	}
	return ballot
}

func buildRevokeNominationBallot() *pbdpos.Ballot {
	ballot := &pbdpos.Ballot{
		Type:    pbdpos.Ballot_TypeRevokeNomination,
		Content: nil,
	}
	return ballot
}

func buildDelegationBallot(voteConf *voteConfig) *pbdpos.Ballot {
	ballot := &pbdpos.Ballot{
		Type: pbdpos.Ballot_TypeDelegation,
		Content: &pbdpos.Ballot_Delegation{Delegation: &pbdpos.DelegationBallotContent{
			AgentAddress: string(voteConf.AgentAddress),
			StartRound:   voteConf.StartRound,
			EndRound:     voteConf.EndRound,
		}},
	}
	return ballot
}

func buildRevokeDelegationBallot() *pbdpos.Ballot {
	ballot := &pbdpos.Ballot{
		Type:    pbdpos.Ballot_TypeRevokeDelegation,
		Content: nil,
	}
	return ballot
}

//parseVoteConfig 解析并校验
func parseVoteConfig(conf *config.ModuleConfig, currentRound int64) (*voteConfig, keystore.Account, error) {
	competeMinerStr := utils.GetStringConfigParameterValue(conf, configParamCompeteMiner, "")
	if competeMinerStr == "" {
		return nil, nil, nil
	}
	if "true" != competeMinerStr && "false" != competeMinerStr {
		return nil, nil, fmt.Errorf("invalid config param value:%s=%s", configParamCompeteMiner, competeMinerStr)
	}
	var err error
	competeMiner, _ := strconv.ParseBool(competeMinerStr)
	var startRound int64
	var endRound int64
	var voterAddress protos.Address
	var voterPeerIDStr string
	var agentAddress protos.Address

	getPositiveIntConfigParamFunc := func(paramName string) (int64, error) {
		paramValStr := utils.GetStringConfigParameterValue(conf, paramName, "")
		if paramValStr == "" {
			return 0, fmt.Errorf("config param '%s' not found or is empty", paramName)
		}
		paramVal, err := strconv.ParseInt(paramValStr, 10, 64)
		if err != nil {
			return 0, err
		}
		if paramVal <= 0 {
			return 0, fmt.Errorf("value of config param '%s' must be positive ", paramName)
		}
		return paramVal, nil
	}

	startRound, err = getPositiveIntConfigParamFunc(configParamStartRound)
	if err != nil {
		return nil, nil, err
	}
	endRound, err = getPositiveIntConfigParamFunc(configParamEndRound)
	if err != nil {
		return nil, nil, err
	}

	if endRound <= startRound {
		return nil, nil, errors.New("startRound must be smaller than endRound")
	}
	if endRound < currentRound {
		return nil, nil, fmt.Errorf("endRound must be greater than currentRound '%d'", currentRound)
	}

	getAddressConfigParamFunc := func(paramName string) (protos.Address, error) {
		paramValStr := utils.GetStringConfigParameterValue(conf, paramName, "")
		if paramValStr == "" {
			return "", fmt.Errorf("config param '%s' not found or is empty", paramName)
		}
		addr := protos.Address(paramValStr)
		if err := addr.Validate(); err != nil {
			return "", err
		}
		return addr, nil
	}

	voterAddress, err = getAddressConfigParamFunc(configParamVoterAddress)
	voterAccount := keystore.GetKeyStore().GetAccount(voterAddress)
	if voterAccount == nil {
		return nil, nil, fmt.Errorf("voter '%s' not found in keystore", voterAddress)
	}

	if competeMiner {
		voterPeerIDStr = utils.GetStringConfigParameterValue(conf, configParamVoterPeerID, "")
		if voterPeerIDStr == "" {
			return nil, nil, fmt.Errorf("networkAddress should be specified if you want to compete miner")
		}
	} else {
		agentAddress, err = getAddressConfigParamFunc(configParamAgentAddress)
		if err != nil {
			return nil, nil, err
		}
	}
	return &voteConfig{
		CompeteMiner: competeMiner,
		StartRound:   startRound,
		EndRound:     endRound,
		VoterAddress: voterAddress,
		VoterPeerID:  voterPeerIDStr,
		AgentAddress: agentAddress,
	}, voterAccount, nil
}
